1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This file contains an analyzer that uses clang-tidy. 7 */ 8 module code_checker.engine.builtin.clang_tidy; 9 10 import logger = std.experimental.logger; 11 import std.algorithm : copy, map, joiner, filter, among; 12 import std.array : appender, array, empty; 13 import std.concurrency : Tid, thisTid; 14 import std.exception : collectException; 15 import std.file : exists; 16 import std.format : format; 17 import std.path : buildPath; 18 import std.process : spawnProcess, wait; 19 import std.range : put, only, enumerate; 20 import std.typecons : Tuple; 21 22 import colorlog; 23 import my.path : AbsolutePath; 24 25 import code_checker.cli : Config; 26 import code_checker.engine.builtin.clang_tidy_classification : CountErrorsResult; 27 import code_checker.engine.file_filter; 28 import code_checker.engine.types; 29 import code_checker.process : RunResult; 30 31 @safe: 32 33 class ClangTidy : BaseFixture { 34 private { 35 Environment env; 36 Result result_; 37 string[] tidyArgs; 38 } 39 40 override string name() { 41 return "clang-tidy"; 42 } 43 44 override string explain() { 45 return "using clang-tidy"; 46 } 47 48 /// The environment the analyzers execute in. 49 override void putEnv(Environment v) { 50 this.env = v; 51 } 52 53 /// Setup the environment for analyze. 54 override void setup() { 55 import std.conv : text; 56 import code_checker.engine.builtin.clang_tidy_classification : filterSeverity, 57 diagnosticSeverity; 58 import code_checker.utility : replaceConfigWords; 59 60 const systemConf = AbsolutePath(only(env.conf.clangTidy.systemConfig) 61 .replaceConfigWords.front); 62 63 auto app = appender!(string[])(); 64 app.put(env.conf.clangTidy.binary); 65 66 app.put("-p=."); 67 68 if (env.conf.clangTidy.applyFixit) { 69 app.put(["--fix"]); 70 } else if (env.conf.clangTidy.applyFixitErrors) { 71 app.put(["--fix-errors"]); 72 } 73 74 if (!env.conf.clangTidy.checkExtensions.empty) 75 ["--checks", env.conf.clangTidy.checkExtensions.joiner(",").text].copy(app); 76 77 env.conf.compiler.extraFlags.map!(a => ["--extra-arg", a]).joiner.copy(app); 78 79 ["--header-filter", env.conf.clangTidy.headerFilter].copy(app); 80 81 if (exists(ClangTidyConstants.confFile) 82 && !isCodeCheckerConfig(AbsolutePath(ClangTidyConstants.confFile))) { 83 logger.infof("Using local '%s' config", ClangTidyConstants.confFile); 84 85 if (env.conf.staticCode.severity != typeof(env.conf.staticCode.severity).min) { 86 logger.warningf("--severity do not work when using a local '%s'", 87 ClangTidyConstants.confFile); 88 } 89 } else { 90 logger.tracef("Writing to %s using %s", ClangTidyConstants.confFile, systemConf); 91 writeClangTidyConfig(systemConf, env.conf); 92 } 93 94 tidyArgs = app.data; 95 } 96 97 /// Execute the analyzer. 98 override void execute() { 99 if (env.conf.clangTidy.applyFixit || env.conf.clangTidy.applyFixitErrors) { 100 executeFixit(env, tidyArgs, result_); 101 } else { 102 executeParallel(env, tidyArgs, result_); 103 } 104 } 105 106 /// Cleanup after analyze. 107 override void tearDown() { 108 } 109 110 /// Returns: the result of the analyzer. 111 override Result result() { 112 return result_; 113 } 114 } 115 116 struct ExpectedReplyCounter { 117 int expected; 118 int replies; 119 120 bool isWaitingForReplies() { 121 return replies < expected; 122 } 123 } 124 125 void executeParallel(Environment env, string[] tidyArgs, ref Result result_) @safe { 126 import core.time : dur; 127 import std.concurrency : Tid, thisTid, receiveTimeout; 128 import std.format : format; 129 import std.parallelism : task, TaskPool; 130 import code_checker.engine.compile_db; 131 import code_checker.engine.logger : Logger; 132 133 bool logged_failure; 134 auto logg = Logger(env.conf.logg.dir); 135 ExpectedReplyCounter cond; 136 137 void handleResult(immutable(TidyResult)* res_) @trusted nothrow { 138 import std.format : format; 139 import std.typecons : nullableRef; 140 import colorlog : Color, color, Background, Mode; 141 import code_checker.engine.builtin.clang_tidy_classification : mapClangTidy; 142 import code_checker.process : exitCodeSegFault; 143 144 auto res = nullableRef(cast() res_); 145 146 logger.infof("%s/%s %s '%s'", cond.replies + 1, cond.expected, 147 "clang-tidy analyzed".color(Color.yellow).bg(Background.black), res.file) 148 .collectException; 149 150 result_.supp += res.suppressedWarnings; 151 152 if (res.clangTidyStatus == 0) { 153 result_.success ~= res.file; 154 } else if (res.clangTidyStatus == exitCodeSegFault) { 155 res.print; 156 result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy segfaulted for " ~ res.file); 157 } else { 158 result_.score += res.errors.score; 159 result_.failed ~= res.file; 160 res.print; 161 162 if (env.conf.logg.toFile) { 163 try { 164 logg.put(res.file, [res.output]); 165 } catch (Exception e) { 166 logger.warning(e.msg).collectException; 167 logger.warning("Unable to log to file").collectException; 168 } 169 } 170 171 if (!logged_failure) { 172 result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy warn about file(s)"); 173 logged_failure = true; 174 } 175 176 try { 177 result_.msg ~= Msg(MsgSeverity.improveSuggestion, 178 format("clang-tidy: %-(%s, %) in %s", res.errors.toRange, res.file)); 179 } catch (Exception e) { 180 logger.warning(e.msg).collectException; 181 logger.warning("Unable to add user message to the result").collectException; 182 } 183 } 184 185 // by treating a segfault as OK it wont block a pull request. this may be a bad idea.... 186 result_.status = mergeStatus(result_.status, res.clangTidyStatus.among(0, 187 exitCodeSegFault) ? Status.passed : Status.failed); 188 } 189 190 auto pool = new TaskPool; 191 scope (exit) 192 pool.finish; 193 194 auto file_filter = FileFilter(env.conf.staticCode.fileExcludeFilter); 195 auto fixedDb = toRange(env); 196 197 foreach (p; fixedDb) { 198 if (!exists(p.cmd.absoluteFile.toString)) { 199 result_.status = Status.failed; 200 result_.score -= 100; 201 result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy where unable to find one of the specified files in compile_commands.json on the filesystem. Your compile_commands.json is probably out of sync. Regenerate it."); 202 break; 203 } else if (!file_filter.match(p.cmd.absoluteFile)) { 204 if (logger.globalLogLevel == logger.LogLevel.all) 205 result_.msg ~= Msg(MsgSeverity.trace, 206 format("Skipping analyze because it didn't pass the file filter (user supplied regex): %s ", 207 p.cmd.absoluteFile)); 208 } else { 209 cond.expected++; 210 211 immutable(TidyWork)* w = () @trusted { 212 return cast(immutable) new TidyWork(tidyArgs, p.cmd.absoluteFile, 213 !env.conf.logg.toFile, env.conf.staticCode.fileExcludeFilter); 214 }(); 215 auto t = task!taskTidy(thisTid, w); 216 pool.put(t); 217 } 218 } 219 220 while (cond.isWaitingForReplies) { 221 () @trusted { 222 try { 223 if (receiveTimeout(1.dur!"seconds", &handleResult)) { 224 cond.replies++; 225 } 226 } catch (Exception e) { 227 logger.error(e.msg); 228 } 229 }(); 230 } 231 } 232 233 /// Run clang-tidy with to fix the code. 234 void executeFixit(Environment env, string[] tidyArgs, ref Result result_) { 235 import code_checker.engine.logger : Logger; 236 import code_checker.engine.compile_db; 237 238 auto logg = Logger(env.conf.logg.dir); 239 240 if (env.conf.logg.toFile) { 241 logg.setup; 242 tidyArgs ~= [ 243 "-export-fixes", buildPath(env.conf.logg.dir, "fixes.yaml") 244 ]; 245 } 246 247 void executeTidy(AbsolutePath file) { 248 auto args = tidyArgs ~ file; 249 logger.tracef("run: %s", args); 250 251 auto status = spawnProcess(args).wait; 252 if (status == 0) { 253 result_.success ~= file; 254 } else { 255 result_.failed ~= file; 256 result_.status = Status.failed; 257 result_.score -= 100; 258 result_.msg ~= Msg(MsgSeverity.failReason, "clang-tidy failed to apply fixes for " 259 ~ file ~ ". Use --clang-tidy-fix-errors to forcefully apply the fixes"); 260 } 261 } 262 263 auto file_filter = FileFilter(env.conf.staticCode.fileExcludeFilter); 264 auto fixedDb = toRange(env); 265 266 const max_nr = fixedDb.length; 267 foreach (idx, cmd; fixedDb.enumerate) { 268 if (!file_filter.match(cmd.cmd.absoluteFile)) { 269 if (logger.globalLogLevel == logger.LogLevel.all) 270 result_.msg ~= Msg(MsgSeverity.trace, 271 format("Skipping analyze because it didn't pass the file filter (user supplied regex): %s ", 272 cmd.cmd.absoluteFile)); 273 } else { 274 logger.infof("File %s/%s %s", idx + 1, max_nr, cmd.cmd.absoluteFile); 275 executeTidy(cmd.cmd.absoluteFile); 276 } 277 } 278 } 279 280 struct TidyResult { 281 AbsolutePath file; 282 CountErrorsResult errors; 283 284 int suppressedWarnings; 285 286 /// Exit status from running clang tidy 287 int clangTidyStatus; 288 289 /// Output to the user 290 string[] output; 291 292 void print() @safe nothrow const scope { 293 import std.ascii : newline; 294 import std.stdio : writeln; 295 296 foreach (l; output) 297 writeln(l).collectException; 298 } 299 } 300 301 struct TidyWork { 302 string[] args; 303 AbsolutePath p; 304 bool useColors; 305 string[] fileExcludeFilter; 306 } 307 308 void taskTidy(Tid owner, immutable TidyWork* work_) nothrow @trusted { 309 import std.concurrency : send; 310 import std.format : format; 311 import code_checker.engine.builtin.clang_tidy_classification : mapClangTidy, 312 mapClangTidyStats, DiagMessage, StatMessage, color; 313 314 auto tres = new TidyResult; 315 TidyWork* work = cast(TidyWork*) work_; 316 317 void sendToOwner() { 318 while (true) { 319 try { 320 owner.send(cast(immutable) tres); 321 break; 322 } catch (Exception e) { 323 logger.tracef("failed sending to: %s", owner).collectException; 324 } 325 } 326 } 327 328 FileFilter file_filter; 329 try { 330 file_filter = FileFilter(work.fileExcludeFilter); 331 } catch (Exception e) { 332 logger.error(e.msg).collectException; 333 tres.clangTidyStatus = -1; 334 sendToOwner; 335 return; 336 } 337 338 try { 339 // there may be warnings that are skipped. If all warnings are skipped 340 // and thus the counter is zero the result should be an automatic 341 // passed. This is because it means that all warnings where from a file 342 // that where excluded. 343 int count_errors; 344 345 bool diagMsg(ref DiagMessage msg) { 346 if (!file_filter.match(msg.file)) 347 return false; 348 349 count_errors++; 350 tres.errors.put(msg.severity); 351 if (work.useColors) 352 msg.diagnostic = format("%s[%s]", msg.diagnostic, color(msg.severity)); 353 else 354 msg.diagnostic = format("%s[%s]", msg.diagnostic, msg.severity); 355 return true; 356 } 357 358 void statMsg(StatMessage msg) { 359 tres.suppressedWarnings = msg.nolint; 360 tres.errors.setSuppressed(msg.nolint); 361 } 362 363 tres.file = work.p; 364 365 auto res = runClangTidy(work.args, [work.p]); 366 367 auto app = appender!(string[])(); 368 mapClangTidy!diagMsg(res.stdout, app); 369 370 mapClangTidyStats!statMsg(res.stderr); 371 372 tres.clangTidyStatus = res.status != 0 ? res.status : count_errors; 373 374 if (tres.clangTidyStatus != 0) { 375 res.stderr.copy(app); 376 tres.output = app.data; 377 } 378 } catch (Exception e) { 379 logger.warning(e.msg).collectException; 380 } 381 382 sendToOwner; 383 } 384 385 struct ClangTidyConstants { 386 static immutable confFile = ".clang-tidy"; 387 static immutable codeCheckerConfigHeader = "# GENERATED by code_checker"; 388 } 389 390 auto runClangTidy(string[] tidy_args, AbsolutePath[] fname) { 391 import code_checker.process; 392 393 auto app = appender!(string[])(); 394 tidy_args.copy(app); 395 fname.copy(app); 396 397 auto rval = run(app.data); 398 if (rval.status == exitCodeSegFault) 399 return run(app.data); 400 return rval; 401 } 402 403 bool isCodeCheckerConfig(AbsolutePath fname) @trusted nothrow { 404 import std.stdio : File; 405 406 try { 407 foreach (l; File(fname).byLine) { 408 return l == ClangTidyConstants.codeCheckerConfigHeader; 409 } 410 return false; 411 } catch (Exception e) { 412 logger.trace(fname).collectException; 413 logger.trace(e.msg).collectException; 414 } 415 416 return false; 417 } 418 419 void writeClangTidyConfig(AbsolutePath baseConf, Config conf) @trusted { 420 import std.file : exists; 421 import std.stdio : File; 422 import std.ascii; 423 import std..string; 424 import code_checker.engine.builtin.clang_tidy_classification : filterSeverity; 425 426 if (!exists(baseConf)) { 427 logger.warning("No default clang-tidy configuration found at ", baseConf); 428 logger.info("Using clang-tidy with default settings"); 429 return; 430 } 431 432 auto fconfig = File(ClangTidyConstants.confFile, "w"); 433 fconfig.writeln(ClangTidyConstants.codeCheckerConfigHeader); 434 435 string[] checks = () { 436 if (conf.staticCode.severity != typeof(conf.staticCode.severity).min) 437 return filterSeverity!(a => a < conf.staticCode.severity).map!(a => "-" ~ a).array; 438 return null; 439 }(); 440 441 if (checks.empty) { 442 foreach (d; File(baseConf).byChunk(4096)) 443 fconfig.rawWrite(d); 444 } else { 445 enum State { 446 other, 447 checkKey, 448 openCheck, 449 insideCheck, 450 closeCheck, 451 afterCheck 452 } 453 454 State st; 455 foreach (l; File(baseConf).byLine) { 456 auto curr = l; 457 458 if (st == State.afterCheck) { 459 fconfig.writeln(l); 460 } else { 461 while (!curr.empty) { 462 const auto old = st; 463 final switch (st) { 464 case State.other: 465 if (curr.startsWith("Checks:")) { 466 st = State.checkKey; 467 } else { 468 fconfig.write(curr[0]); 469 curr = curr[1 .. $]; 470 } 471 break; 472 case State.checkKey: 473 if (curr[0].among('"', '\'')) { 474 st = State.openCheck; 475 } else { 476 fconfig.write(curr[0]); 477 curr = curr[1 .. $]; 478 } 479 break; 480 case State.openCheck: 481 fconfig.write(curr[0]); 482 curr = curr[1 .. $]; 483 st = State.insideCheck; 484 break; 485 case State.insideCheck: 486 if (curr[0].among('"', '\'')) { 487 st = State.closeCheck; 488 } else { 489 fconfig.write(curr[0]); 490 curr = curr[1 .. $]; 491 } 492 break; 493 case State.closeCheck: 494 curr = curr[1 .. $]; 495 st = State.afterCheck; 496 break; 497 case State.afterCheck: 498 fconfig.write(curr[0]); 499 curr = curr[1 .. $]; 500 break; 501 } 502 503 debug logger.tracef(old != st, "%s -> %s : %s", old, st, curr); 504 505 if (st == State.closeCheck) { 506 fconfig.writeln(",\\"); 507 fconfig.write(checks.joiner(",")); 508 fconfig.write(curr[0]); 509 } 510 } 511 512 fconfig.writeln; 513 } 514 } 515 fconfig.writeln; 516 } 517 518 foreach (kv; conf.clangTidy.optionExtensions.byKeyValue) { 519 fconfig.writeln(" - key: ", kv.key); 520 fconfig.writefln(" value: '%s'", kv.value); 521 } 522 }